<!DOCTYPE html>
<html lang="zh">
	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<meta http-equiv="X-UA-Compatible" content="ie=edge">
		<title>d3测试</title>
		<script src="d3.js" type="text/javascript" charset="utf-8"></script>
		<style type="text/css">
			.myrect,rect {fill: #42B983;}
			text {fill: #008000; font-weight: bold;}
			.over{fill: yellowgreen;} .out {fill: #42B983}
		</style>
	</head>
	<body>
		<svg width=960 height=600 style="border: 1px solid;"></svg>
	</body>
	<script type="text/javascript">
		var svgPadding = 30;
		var svg = d3.select('svg'), g = svg.append('g').attr('transform', `translate(${svgPadding}, ${svgPadding})`);
		var innerHeight = svg.attr('height') - svgPadding*2, innerWidth = svg.attr('width')-svgPadding*2;
		
		treeShap();
			
		function treeShap(){
			/* 
			 为了绘制一个树状图，我们还是需要以下新的知识点
			d3.hierarchy()，层级布局，需要和tree生成器一起使用，来得到绘制树所需要的节点数据和边数据
			d3.hierarchy().sum() ,后序遍历。这里给出官方API的解释，虽然对于绘制一个基本的树状图用不到这个函数
			d3.tree()，创建一个树状图生成器
			d3.tree().size()，定义树的大小
			d3.tree().separation(),定义邻居节点的距离
			node.descendants()得到所有的节点，已经经过转换的数据
			node.links()，得到所有的边，已经经过转换的数据
			d3.linkHorizontal()，创建一个贝塞尔生成曲线生成器，当然不止只有水平的，还有垂直的（d3.linkVertical()），这里给出官方API的示例
			 */
			var dataset = {
				name:"中国",
				children:[
					{
						name:"浙江",
						children:[
							{name:"杭州" ,value:100},
							{name:"宁波",value:100},
							{name:"温州",value:100},
							{name:"绍兴",value:100}
						]
					},
					{
						name:"广西",
						children:[
							{
								name:"桂林",
								children:[
									{name:"秀峰区",value:100},
									{name:"叠彩区",value:100},
									{name:"象山区",value:100},
									{name:"七星区",value:100}
								]
							},
							{name:"南宁",value:100},
							{name:"柳州",value:100},
							{name:"防城港",value:100}
						]
					},
					{
						name:"黑龙江",
						children:[
							{name:"哈尔滨",value:100},
							{name:"齐齐哈尔",value:100},
							{name:"牡丹江",value:100},
							{name:"大庆",value:100}
						]
					},
					{
						name:"新疆" , 
						children:
						[
							{name:"乌鲁木齐"},
							{name:"克拉玛依"},
							{name:"吐鲁番"},
							{name:"哈密"}
						]
					}
				]
			}
			var hierarchyData = d3.hierarchy(dataset).sum(d=>d.value)
			console.log("hierarchyData", hierarchyData);
			// 创建树状图
			var tree = d3.tree().size([innerWidth-400, innerHeight-400]).separation((a,b)=>(a.parent==b.parent?1:2)/a.depth);
			// 初始化树状图
			var treeData = tree(hierarchyData); 
			console.log('treeData', treeData);
			let nodes = treeData.descendants(), links = treeData.links();
			console.log(nodes, links);
			// 创建一个贝塞尔生成曲线生成器
			var Bézier_curve_generator = d3.linkHorizontal().x(d=>d.y).y(d=>d.x); // 垂直的不需要转换x和y的位置
			
			g = svg.append('g').attr('transform', `translate(100, ${svgPadding})`);
			// 绘制边
			g.append('g').selectAll('path').data(links).enter().append('path')
				.attr('d', d=> {
					let start = {x:d.source.x, y: d.source.y};
					let end = {x:d.target.x, y: d.target.y};
					return Bézier_curve_generator({source: start, target: end})
				})
				.attr('fill', 'none').attr('stroke', 'yellow').attr('stroke-width', 1);
			// 绘制节点和文字
			var gs = g.append('g').selectAll('g').data(nodes).enter().append('g')	
				.attr('transform', d=>`translate(${d.y}, ${d.x})`) // 这里注意x和y颠倒的
				
			gs.append('circle').attr('r',6).attr('fill', 'white').attr('stroke', 'blue').attr('stroke-width', 1);
			gs.append('text').attr('x',d=>d.children?-40:8) // 如果某节点有子节点，则对应的文字前移
				.attr('y', -5).attr('dy', 10)
				.text(d=>d.data.name);
		}
		// 力导向图
		function simulationShap(){
			//准备数据
			var nodes = [//节点集
				{name:"湖南邵阳"},
				{name:"山东莱州"},
				{name:"广东阳江"},
				{name:"山东枣庄"},
				{name:"泽"},
				{name:"恒"},
				{name:"鑫"},
				{name:"明山"},
				{name:"班长"}
			];
			
			var edges = [//边集
				{source:0,target:4,relation:"籍贯",value:1.3},
				{source:4,target:5,relation:"舍友",value:1},
				{source:4,target:6,relation:"舍友",value:1},
				{source:4,target:7,relation:"舍友",value:1},
				{source:1,target:6,relation:"籍贯",value:2},
				{source:2,target:5,relation:"籍贯",value:0.9},
				{source:3,target:7,relation:"籍贯",value:1},
				{source:5,target:6,relation:"同学",value:1.6},
				{source:6,target:7,relation:"朋友",value:0.7},
				{source:6,target:8,relation:"职责",value:2}
			];
			// 设置颜色比例尺
			var colorScale = d3.scaleOrdinal().domain(d3.range(nodes.length)).range(d3.schemeCategory10);		
			// 新建一个力导向图： 对于d3.forceSimulation().force(name)，也就是当force中只有一个参数，这个参数是某个力的名称，那么这段代码返回的是某个具体的力
			var forceSimulation = d3.forceSimulation()
			    		.force("link",d3.forceLink())
			    		.force("charge",d3.forceManyBody())
			    		.force("center",d3.forceCenter());
			//初始化力导向图，也就是传入数据
			//生成节点数据
			forceSimulation.nodes(nodes).on("tick",ticked);//这个函数很重要，后面给出具体实现和说明
			//生成边数据，对edges进行转换
			forceSimulation.force("link").links(edges).distance(d=>d.value*100) //每一边的长度  	
			//设置图形的中心位置	
			forceSimulation.force("center").x(innerWidth/2).y(innerHeight/2);
			//在浏览器的控制台输出
			/*
			   name: "湖南邵阳"
			   index: 0
			   x: 432.13862786389143
			   y: 179.8659092655551
			   vy: -0.000031943768780717426
			   vx: -0.0002826834906357848
			*/
			console.log(nodes);
			/* 
			 source: {name: "湖南邵阳", index: 0, x: 432.13862786389143, y: 179.8659092655551, vy: -0.000031943768780717426, …}
			 target: {name: "泽", index: 4, x: 300.9356670256005, y: 171.15571616120772, vy: 0.0004293058834733494, …}
			 relation: "籍贯"
			 value: 1.3
			 index: 0
			 */
			console.log(edges);
			
			//有了节点和边的数据后，我们开始绘制
			//绘制边 这里注意一下，应该先绘制边，在绘制顶点，因为在d3中，各元素是有层级关系的，先绘制的在下面
			var links = g.append("g").selectAll("line").data(edges).enter().append("line")
				.attr("stroke", (d,i)=>colorScale(i))
				.attr("stroke-width",1);
			var linksText = g.append("g").selectAll("text").data(edges).enter()
				.append("text")
				.text(d=>d.relation);
			
			//绘制节点
			//老规矩，先为节点和节点上的文字分组
			var gs = g.selectAll(".circleText").data(nodes).enter().append("g")
				.attr("transform", (d,i) => `translate(${d.x}, ${d.y})`)
				/* drag中有三个函数，在这里进行了实现，其中d.fx和d.fy表示固定坐标，
					例如，现在我们看到dragged函数，我们可以发现这样的代码：
					d.fx = d3.event.x;  d.fy = d3.event.y;，也就是在拖动节点的时候，鼠标位置在哪里（d3.event），节点的固定位置就在哪里，
					再看到ended函数，也就是结束拖动的时候触发，可以发现，固定坐标都为空，也就是不固定，这样模拟的效果才好（你们也可以试试去掉ended函数会发生什么，这样可以更好的理解）
				*/
				.call(d3.drag()
					.on("start", function(d) {
						if(!d3.event.active){
							forceSimulation.alphaTarget(0.8).restart();
						}
						d.fx = d.x;
						d.fy = d.y;
					})
					.on("drag", function(d){
						d.fx = d3.event.x;
						d.fy = d3.event.y;
					})
					.on("end", function(d){
						if(!d3.event.active){
							forceSimulation.alphaTarget(0);
						}
						d.fx = null;
						d.fy = null;
					})
				);
				
			//绘制节点
			gs.append("circle").attr("r",10).attr("fill",(d,i)=>colorScale(i))
			//文字
			gs.append("text").attr("x",-10).attr("y",-20).attr("dy",10).text(d=>d.name)
			
			// 因为力导向图是不断运动的，每一时刻都在发生更新，所以需要不断更新节点和连线的位置
			function ticked(){
				links
					.attr("x1", d=>d.source.x).attr("y1",d=>d.source.y)
					.attr("x2",d=>d.target.x).attr("y2",d=>d.target.y);
					
				linksText
					.attr("x", d=>(d.source.x+d.target.x)/2)
					.attr("y", d=>(d.source.y+d.target.y)/2);
					
				gs.attr("transform",d=>`translate(${d.x}, ${d.y})`);
			}
		}
		
		// 饼状图
		function pieShap(){
			var dataset = [ 30 , 10 , 43 , 55 , 13 ];//需要将这些数据变成饼状图的数据
			// 设置颜色比例尺
			var colorScale = d3.scaleOrdinal().domain(dataset).range(d3.schemeCategory10);
			//新建一个饼状图
			var pie = d3.pie();
			
			//新建一个弧形生成器
			var innerRadius = 50;//内半径
			var outerRadius = 100;//外半径
			var arc_generator = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius);
			//将原始数据变成可以绘制饼状图的数据，
			var pieData = pie(dataset);
			console.log(pieData);
			var gs = g.selectAll('g').data(pieData).enter().append('g')
				.attr('transform', `translate(${innerWidth/2},${innerHeight/2})`);
			// 绘制扇形
			gs.append('path').attr('d', d=>arc_generator(d))
				.attr('fill', (d,i)=> colorScale(d.data)) // 这里的d是pieData，colorScale(d)返回的是不变的值
			// 添加文字信息
			gs.append('text').attr('transform', d=>`translate(${arc_generator.centroid(d)})`) // 位置设置在中心处
				// 水平居中：text-anchor = "middle" 垂直居中：dominant-baseline="middle"
				.attr('text-anchor', 'middle')
				.text(d=>d.data);
		}
		
		// 事件交互
		function demoEventInteraction(){
			let dataset = d3.range(0,100,10);
			var scale = d3.scaleBand().domain(dataset).rangeRound([0, innerHeight]);
			g.selectAll('rect').data(dataset).enter().append('rect')
				.attr('x', 0).attr('y', (d,i)=>scale.step()*i)
				.attr('width', (d,i)=>scale(d))
				.attr('height', ()=>scale.step()-10)
				.on('mouseover', function(){ d3.select(this).transition().duration(1500).attr('class', 'over')})
				.on('mouseout', function(){ d3.select(this).transition().duration(1500).attr('class', 'out')})
				;
		}
		
		// 我的自定义柱状图，动画，横坐标中文，12个月
		function myFullDateRectDemo(){
			var dataset = [];
			for(let i=0; i<12; i++) {
				dataset[i] = Math.floor(Math.random()*10000)/100;
			}
			var svgPadding = 40, rectPadding = 10;
			// 创建分组，边距40
			var svg = d3.select('svg'), g = svg.append('g').attr('transform', `translate(${svgPadding}, ${svgPadding})`);
			var svgWidth = svg.attr('width'), svgHeight = svg.attr('height'), innerSvgHeight = svgHeight - svgPadding*2;
			
			// 12个月的横坐标；每个月与数字一一对应，对应的数字代表了translate中的x值，相当于间距了
			var xScale = d3.scaleBand().domain(d3.range(1,13).map(d=>d+'月')).rangeRound([0, svgWidth - svgPadding*2]);
			// xScale('1月'); 获取到的是1月的值所对应的range值，这里是0
			
			// y轴比例尺
			var yScale = d3.scaleLinear().domain([0, d3.max(dataset)]).rangeRound([innerSvgHeight,0]);
			
			var gs = g.selectAll('g').data(dataset).enter().append('g');
			
			gs.append('rect')
				.attr('x', (d,i)=>xScale.step()*i)
				.attr('y', innerSvgHeight).attr('height', 0)
				// 添加动画过渡： d3.easeBounce d3.easeElasticInOut
				.transition().duration(2000).delay((d,i)=>i*400).ease(d3.easeBounce)
				.attr('y', (d)=> yScale(d))
				.attr('width', xScale.step() - rectPadding)
				.attr('height', (d,i)=>innerSvgHeight - yScale(d));
			gs.append('text')
				.attr('x', (d,i)=>xScale.step()*i)
				.attr('y', 0) // 0 是从上往下落；总高度innerSvgHeight则是从下往上升
				.transition().duration(2000).delay((d,i)=>i*400).ease(d3.easeBounce)
				.attr('y', (d)=> yScale(d))
				.attr('dy', 15)
				.text(d=>d);
			// 添加横向坐标轴
			g.append('g').attr('transform', `translate(${-rectPadding/2}, ${innerSvgHeight})`).call(d3.axisBottom(xScale))
			// 添加纵坐标
			g.append('g').call(d3.axisLeft(yScale).ticks(12));
			// var g = d3.select('svg').append('g').attr('transform', 'translate(0, 50)').call(d3.axisTop(xScale));
		}
		// 完整的柱状图（包含横纵坐标和文字），还有动态效果
		function renderFullRect(){
			var svgPadding = 40;
			var svg = d3.select('svg'), g = svg.append('g').attr('transform', `translate(${svgPadding},${svgPadding})`);
			var svgHeight = svg.attr('height');
			var dataset = [10,20,30,23,13,40,27,35,20];
			// d3.range重点了解, linear.rangeRound - 设置比例尺的输出范围，并四舍五入。
			// d3.range(start, stop, step) step没有设置，默认是1
			// v4 .scaleBand 取代了 v3 .scale.ordinal 序数比例尺
			var xScale = d3.scaleBand().domain(d3.range(dataset.length)).rangeRound([0, svg.attr('width')-svgPadding -svgPadding]);
			var xAxis = d3.axisBottom(xScale);
			// range从大到小，对应的值反比例关系，对应y轴上，y越大，其实高度越低
			var yScale = d3.scaleLinear().domain([0,d3.max(dataset)]).range([svgHeight-svgPadding-svgPadding, 0]);
			var yAxis = d3.axisLeft(yScale);
			var rectPadding = 20;//矩形之间的间隙
			// 创建分组（包含矩形和文字）
			var gs = g.selectAll('g').data(dataset).enter().append('g');
			gs.append('rect')
				// bandwidth 每個分段的寬度  step 相鄰兩個分段之前的距離
				.attr('width', d=>xScale.step()-rectPadding) //宽度=比例值-间隙 这里用bandwidth也是一样的
				.attr('class', 'myrect')
				.attr('x', (d,i)=> {console.log(xScale(i)); return xScale(i)+rectPadding/2})
				.attr('y', (d,i)=>{ var min=yScale.domain()[0]; return yScale(min); }) // 获取y最小值
				.attr('height',0)
				// 添加动图效果
				.transition().delay((d,i)=>i * 200).duration(2000).ease(d3.easeBounce)
				.attr('y', d=>yScale(d))
				.attr('height', d=>svgHeight - svgPadding - svgPadding -yScale(d)) // -svgPadding-svgPadding 代表top和bottom，svg的内边距
				;
			gs.append('text')
				.attr('x', (d,i)=>xScale(i))
				.attr('dx', ()=>(xScale.step()-rectPadding)/2) // 柱状居中
				.attr('dy',20) // 向下偏移20，距离柱图顶部
				.attr('y', (d,i)=>{ var min=yScale.domain()[0]; return yScale(min); })
				.transition()
				.delay(function(d,i) {
					return i*200
				})
				.duration(2000)
				.ease(d3.easeCubic)
				.attr('y', d=>yScale(d))
				.text(d=>d);
			// 添加坐标轴	
			g.append('g').attr('transform', `translate(0, ${svgHeight-svgPadding-svgPadding})`).call(xAxis);
			g.append('g').call(yAxis);
			
		}
		// 使用比例尺和坐标轴构建柱状图
		function renderRectByScale(){
			var dataset = [1.8, 2.5 , 0.4 , 2.1 , 3.5, 1.7 , 1.3 , 0.9 , 2.8];  
			var svg = d3.select('svg'), g = svg.append('g').attr('transform', `translate(50, 30) `);
			var y = 300;
			
			//定义一个线性比例尺
			var scaleLinear = d3.scaleLinear().domain([0,d3.max(dataset)]).range([0,300]);
			
			g.selectAll('rect').data(dataset).enter().append('rect')
				.attr('x', (d,i)=>i*40)
				.attr('y', d=>y-scaleLinear(d)) 
				.attr('width', 30)
				.attr('height', d=>scaleLinear(d))
				.attr('class', 'myrect');
				
			// 定义一个坐标轴
			// var xAxis = d3.axisTop(scaleLinear) // 定义axis top表示向上
			// 				.ticks(dataset.length*2); // 设置刻度数目
			// // 添加坐标轴 （相当于逆时针旋转90度，竖着展示）
			// g.append('g').attr('transform', `translate(-10, ${y}) rotate(270)`).call(xAxis);
			var yAxis = d3.axisLeft(d3.scaleLinear().domain([0,d3.max(dataset)]).range([300,0])).ticks(dataset.length*2);
			g.append('g').attr('transform', 'translate(-10)').call(yAxis)
		}
		
		// 序数比例尺： domain域和range域是离散的，也就是数组
		function scaleOrdinalTest1(){
			var index = [0,1,2,3,4],color = ["red","blue","yellow","black","green"];
			var scaleOrdinal = d3.scaleOrdinal().domain(index).range(color);
			
			document.write("scaleOrdinal(1)输出：" + scaleOrdinal(1));
			d3.select('body').append('br');
			document.write("scaleOrdinal(2)输出：" + scaleOrdinal(2));
			d3.select('body').append('br');
			document.write("scaleOrdinal(4)输出：" + scaleOrdinal(4));
		}
		// 线性比例尺
		function scaleLinear1(){
			var ds = [1.2,2.3,0.9,1.5,3.3], min = d3.min(ds), max = d3.max(ds);
			//[0.9,3.3]映射 [0,300]
			var scaleLinear = d3.scaleLinear().domain([min, max]).range([0, 300]);
			document.write("scaleLinear(1)输出：" + scaleLinear(1));
			d3.select('body').append('br');
			document.write("scaleLinear(2)输出：" + scaleLinear(2));
			d3.select('body').append('br');
			document.write("scaleLinear(3.3)输出：" + scaleLinear(3.3));
		}
		// 递增阶梯柱状图
		function renderRect2(){
			var svg = d3.select('svg'),
				g = svg.append('g').attr('transform', 'translate(100, 100)')
			var y = 200;
			g.selectAll('rect').data([1,2,3,4,5,6])
				.enter()
				.append('rect')
				.attr('y', d=>y-d*30).attr('x', (d,i)=> i * 40)
				.attr('height', d=>d*30).attr('width', 30).attr('class', 'myrect');
		}
		// 横向递减柱状图
		function renderRect1(){
			var marge = {
				top: 60,
				bottom: 60,
				left: 60,
				right: 60
			} //设置边距
			var dataset = [250, 210, 170, 130, 90]; //数据（表示矩形的宽度）
			var svg = d3.select('svg'),
				g = svg.append('g').attr('transform', `translate(${marge.top}, ${marge.left})`); // translate 设置位置
			
			var rectHeight = 30; // 矩形高度
			g.selectAll('rect').data(dataset).enter().append('rect')
				.attr('x', 20) // 设置左上点的x
				.attr('y', (d, i)=> {
					return i * rectHeight;
				})
				.attr('width', d=>d)
				.attr('height', rectHeight - 5)
				.attr('class', 'myrect');
		}
	</script>
</html>
